package com.googlecode.vmock;

import java.lang.reflect.AccessibleObject;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.ConstructorSignature;
import org.aspectj.lang.reflect.MethodSignature;

import com.googlecode.vmock.exceptions.MockVerifyException;

/**
 * An aspect that catches all the calls to the methods outside the VMock package.
 * 
 * @author marcin.jagus
 */
@Aspect
public class CallInterceptorAspect {

    /**
     * Matches calls to everything.
     */
    @Pointcut("call(* *.*(..))")
    public void matchCalls() {
    }

    /**
     * Matches all constructor calls.
     */
    @Pointcut("call(*.new(..))")
    public void matchConstructorCalls() {
    }

    /**
     * Matches all calls made to methods in <code>com.googlecode.vmock</code> and <code>java.lang</code> packages.
     */
    @Pointcut("call(* com.googlecode.vmock..*.*(..)) || call(* java.lang..*.*(..))")
    public void matchSkippableCalls() {
    }

    /**
     * Matches all calls made to constructors in <code>com.googlecode.vmock</code> and <code>java.lang</code> packages.
     */
    @Pointcut("call(com.googlecode.vmock..*.new(..)) || call(java.lang..*.new(..))")
    public void matchSkippableConstructors() {
    }

    /**
     * Matches classes in <code>com.googlecode.vmock</code> and <code>net.sf.cglib</code> packages and its subpackages.
     */
    @Pointcut("within(com.googlecode.vmock..*) || within(net.sf.cglib..*)")
    public void matchSkippablePackages() {
    }

    /**
     * Matches every join point of a control flow that starts at the beggining of
     * {@link CallInterceptorAspect#handleInterceptedCall(ProceedingJoinPoint)} method.
     */
    @Pointcut("cflow(execution(* com.googlecode.vmock.CallInterceptorAspect.handleInterceptedCall(..)))")
    public void matchSkippableFlows() {
    }

    /**
     * Matches all CGLIB generated proxies.
     */
    @Pointcut("within(*..*.*CGLIB*)")
    public void matchCGLIB() {
    }

    /**
     * The heart of {@link VMock}. Intercepts a call and returns mocked result if applicable. Calls the original method otherwise.
     * 
     * @param pjp
     * @return Either a mocked result or a result from calling the original method.
     * @throws Throwable
     */
    @Around("(matchCalls() || matchConstructorCalls()) && !matchSkippableCalls() && !matchSkippableConstructors() && !matchSkippablePackages() && !matchSkippableFlows() && !matchCGLIB()")
    public Object interceptedCall(ProceedingJoinPoint pjp) throws Throwable {
        Object result = handleInterceptedCall(pjp);

        if (result == InterceptionResult.NOT_OVERRIDEN) {
            return pjp.proceed();
        } else if (result == InterceptionResult.INTERCEPT_RECORDING) {
            return callWithRecordingLogic(pjp);
        } else if (result == InterceptionResult.INTERCEPT_REPLAYING) {
            return callWithReplayingLogic(pjp);
        } else {
            return result;
        }
    }

    /**
     * Returns a mocked result or an object which informs the caller that the call was not mocked in the {@link MockStage#RECORDING} stage.
     * 
     * @param pjp
     * @return Either a mocked result or {@link InterceptionResult#NOT_OVERRIDEN} object
     * @throws Throwable
     */
    protected Object handleInterceptedCall(ProceedingJoinPoint pjp) throws Throwable {
        AccessibleObject calledObject;
        if (pjp.getSignature() instanceof MethodSignature) {
            MethodSignature ms = (MethodSignature) pjp.getSignature();
            calledObject = ms.getMethod();
        } else {
            ConstructorSignature cs = (ConstructorSignature) pjp.getSignature();
            calledObject = cs.getConstructor();
        }

        MockController controller = MockController.getInstance();

        if (controller != null && pjp.getThis() != null) {
            InterceptedCall call = new InterceptedCall(pjp.getThis(), pjp.getTarget(), calledObject);
            InterceptionResult result = controller.handleInterceptedCall(call, pjp.getArgs());

            if (result.isOverridenResult()) {
                return result.getResult();
            } else {
                return result;
            }
        } else {
            return InterceptionResult.NOT_OVERRIDEN;
        }
    }

    /**
     * Wraps a call to a {@link Record} constructor with recording logic. This means that after the call is made, {@link MockController} will move
     * from RECORDING stage to RECORDING_PAUSED stage.
     * 
     * @param pjp
     * @return {@link Record} instance
     * @throws Throwable
     */
    protected Object callWithRecordingLogic(ProceedingJoinPoint pjp) throws Throwable {
        // Recording must begin in the Record class constructor, where 'this' object is available.
        try {
            return pjp.proceed();
        } finally {
            MockController.getInstance().pauseRecording();
        }
    }

    /**
     * Wraps a call to a {@link Replay} constructor with replaying logic. This means that before the call, {@link MockController} will move to
     * REPLAYING stage. After the call, {@link MockController} will move to VERYFING stage. This means that this method may also throw
     * {@link MockVerifyException}.
     * 
     * @param pjp
     * @return {@link Replay} instance
     * @throws Throwable
     */
    protected Object callWithReplayingLogic(ProceedingJoinPoint pjp) throws Throwable {
        MockController.getInstance().replay();
        Object result = pjp.proceed();
        MockController.getInstance().verify();

        return result;
    }
}
